歡迎來到第十三天!昨天的內容相當輕鬆對吧! 我也覺得,但串接第三方就是很容易有些意外嘛,多留點空總不會錯!另一方面就是家中出了點事情,我需要一點時間去處理,這幾天的內容我會盡量放輕鬆一點,但不至於影響到我系列文的規劃,不用擔心!
昨天我們主要是建立了 Judge0的帳號並透過他提供的getLanguges API 測試了是否成功串接,從那一長串支援的語言清單來看是挺成功的!但我們要的可遠遠不止有這樣對吧!重點是要讓他執行程式碼嘛!今天,我們就要來做這件最關鍵的事:將使用者在前端編輯器中寫的 JavaScript 程式碼,安全地傳送給 Judge0,讓它在隔離的沙箱環境中執行,然後把詳細的「實驗報告」——也就是程式碼的客觀執行結果——拿回來。
這份報告包含了程式碼究竟是成功運行、還是噴出錯誤、輸出了什麼內容、花了多少時間等鐵證如山的客觀事實。這些事實,將是我們下一階段餵給 AI,讓它做出高品質 Code Review 的最重要依據。
/api/judge0/execute/route.ts。stdout, stderr, time, memory 等客觀數據。在開始寫程式碼之前,我們必須先理解 Judge0 處理程式碼執行的模式。它並不像一個簡單的函式呼叫,你傳入參數後就立刻得到回傳值。由於執行程式碼可能需要時間(從幾毫秒到幾秒鐘不等),Judge0 採用的是一個非同步 (Asynchronous) 的兩階段流程:
第一階段:提交任務,取得「取餐號碼牌」
POST /submissions 端點發出請求,請求中包含我們要執行的程式碼、語言 ID 等資訊。第二階段:憑號碼牌,輪詢取餐
token 後,需要對 GET /submissions/{token} 這個端點發出請求。token 查詢一次。這個重複查詢的過程,就叫做輪詢 (Polling)。stdout, stderr 等資訊的完整報告。這個流程就像你去手搖飲店點餐,店員會給你一個號碼牌(token),然後你會盯著叫號螢幕(輪詢),直到你的號碼出現(執行完成),你才能去取餐(取得結果)。
理解了流程後,我們來實作這個核心代理。根據昨天的經驗,我們知道 API 路由必須放在以它命名的資料夾底下。
請在 app/api/judge0/ 資料夾下,建立一個新的資料夾 execute,並在其中建立檔案 route.ts。
檔案路徑: app/api/judge0/execute/route.ts
// app/api/judge0/execute/route.ts
import { NextResponse } from 'next/server';
const JUDGE0_API_HOST = process.env.JUDGE0_API_HOST;
const JUDGE0_API_KEY = process.env.JUDGE0_API_KEY;
// JavaScript 的語言 ID 在 Judge0 中是 93 (Node.js 18.15.0)
const JAVASCRIPT_LANGUAGE_ID = 93;
// 輔助函式:用於延遲
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
export async function POST(request: Request) {
  if (!JUDGE0_API_HOST || !JUDGE0_API_KEY) {
    return NextResponse.json(
      { error: 'Judge0 API 環境變數未設定' },
      { status: 500 }
    );
  }
  try {
    const { source_code } = await request.json();
    if (!source_code) {
      return NextResponse.json({ error: '缺少 source_code' }, { status: 400 });
    }
    // --- Step 1: 提交程式碼 (Base64 編碼) ---
    const encodedSourceCode = Buffer.from(source_code).toString('base64');
    const submissionResponse = await fetch(
      // 關鍵:base64_encoded=true
      `https://${JUDGE0_API_HOST}/submissions?base64_encoded=true&wait=false`,
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-RapidAPI-Key': JUDGE0_API_KEY,
          'X-RapidAPI-Host': JUDGE0_API_HOST,
        },
        body: JSON.stringify({
          source_code: encodedSourceCode, // 送出編碼後的程式碼
          language_id: JAVASCRIPT_LANGUAGE_ID,
        }),
      }
    );
    if (!submissionResponse.ok) {
      const errorText = await submissionResponse.text();
      console.error('Judge0 submission failed:', errorText);
      return NextResponse.json(
        { error: '提交至 Judge0 失敗', details: errorText },
        { status: submissionResponse.status }
      );
    }
    const submissionResult = await submissionResponse.json();
    const { token } = submissionResult;
    if (!token) {
      return NextResponse.json(
        { error: '無法從 Judge0 取得 Token' },
        { status: 500 }
      );
    }
    // --- Step 2: 輪詢 (Polling) 取得執行結果 ---
    let resultData;
    const maxRetries = 10;
    const retryDelay = 500;
    for (let i = 0; i < maxRetries; i++) {
      await sleep(retryDelay);
      const resultResponse = await fetch(
        // 關鍵:base64_encoded=true
        `https://${JUDGE0_API_HOST}/submissions/${token}?base64_encoded=true`,
        {
          method: 'GET',
          headers: {
            'X-RapidAPI-Key': JUDGE0_API_KEY,
            'X-RapidAPI-Host': JUDGE0_API_HOST,
          },
        }
      );
      if (!resultResponse.ok) {
        const errorText = await resultResponse.text();
        return NextResponse.json(
          { error: '從 Judge0 獲取結果失敗', details: errorText },
          { status: resultResponse.status }
        );
      }
      resultData = await resultResponse.json();
      if (resultData.status_id > 2) {
        // 1: In Queue, 2: Processing
        break; // 執行完成 (成功、失敗、超時等)
      }
    }
    if (!resultData || resultData.status_id <= 2) {
      return NextResponse.json({ error: '程式碼執行超時' }, { status: 408 });
    }
    // --- Step 3: 解碼 Base64 結果 ---
    const decodedResult = {
      ...resultData,
      stdout: resultData.stdout
        ? Buffer.from(resultData.stdout, 'base64').toString('utf-8')
        : null,
      stderr: resultData.stderr
        ? Buffer.from(resultData.stderr, 'base64').toString('utf-8')
        : null,
      compile_output: resultData.compile_output
        ? Buffer.from(resultData.compile_output, 'base64').toString('utf-8')
        : null,
    };
    return NextResponse.json(decodedResult);
  } catch (error) {
    console.error('代理 /api/judge0/execute 錯誤:', error);
    return NextResponse.json({ error: '代理伺服器內部錯誤' }, { status: 500 });
  }
}
整個流程可以拆解成三個主要階段:提交、輪詢和解碼。
在函式主體開始前,我們定義了幾個關鍵常數:
當 POST 請求進來後,在驗證過必要的參數後,我們立刻進入提交流程:
Buffer.from(...).toString('base64')):這是整個流程中最關鍵的部分。我們將前端傳來的 source_code 字串轉換成 Base64 格式。這就像是把一份內容特殊的文件(可能包含中文、emoji 等)放進一個標準化的堅固信封裡,確保它在網路傳輸過程中不會因為編碼問題而出錯。
POST 請求至 /submissions:拿到 token 後,我們不能傻等,而是要主動去「叫號」。這就是輪詢機制:
GET 請求至 /submissions/{token}: 我們用 token 去查詢特定任務的進度。同樣地,我們也需要加上 base64_encoded=true 來告訴 Judge0 我們期望收到的回傳內容(如 stdout)也是 Base64 格式。resultData.status_id > 2): 這是輪詢的核心邏輯。根據 Judge0 的文件,狀態 1 和 2 分別代表「排隊中」和「處理中」。任何大於 2 的狀態都意味著執行已結束(無論成功、失敗或超時)。一旦任務完成,我們就用 break 跳出迴圈。408 Request Timeout 錯誤給前端。一旦拿到執行完畢的結果,我們還需要做最後一步處理才能回傳給前端:
Buffer.from(..., 'base64').toString('utf-8')):NextResponse: 最後,我們將這個處理乾淨、完全解碼的 decodedResult 物件作為 JSON 回應,傳回給前端。為了在不修改前端的情況下,我們先快速測試這個新的代理 API,我們來建立一個獨立的 Node.js 腳本。
在專案根目錄的 scripts/ 資料夾下,建立一個新檔案 test-execution.js。
// scripts/test-execution.js
async function runTest(description, source_code) {
  console.log(`\n--- 測試案例: ${description} ---`);
  try {
    const response = await fetch('http://localhost:3000/api/judge0/execute', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ source_code }),
    });
    if (!response.ok) {
      const errorText = await response.text();
      console.error(`  ❌ 請求失敗 (${response.status}):`);
      console.error('--- Server Response ---');
      console.error(errorText);
      console.error('--- End Server Response ---');
      return;
    }
    const data = await response.json();
    console.log('  ✅ 成功從代理 API 收到回應:');
    const relevantData = {
      stdout: data.stdout,
      stderr: data.stderr,
      status: data.status,
      time: data.time,
      memory: data.memory,
    };
    console.log(JSON.stringify(relevantData, null, 2));
  } catch (error) {
    console.error('  ❌ 執行測試時發生網路或其他錯誤:', error.message);
  }
}
async function main() {
  // 測試 1: 正常的 console.log
  const codeSuccess = `console.log('Hello from the secure sandbox!');`;
  await runTest('正常執行的程式碼', codeSuccess);
  // 測試 2: 會產生執行階段錯誤 (Runtime Error) 的程式碼,並包含中文註解
  const codeError = `const a = 1;\na.toUpperCase(); // 這會產生 TypeError`;
  await runTest('會產生執行階段錯誤 (Runtime Error) 的程式碼', codeError);
  // 測試 3: 語法錯誤的程式碼
  const codeSyntaxError = `console.log('Missing quote);`;
  await runTest('語法錯誤 (Syntax Error) 的程式碼', codeSyntaxError);
  // 測試 4: 包含中文字串的正常程式碼
  const codeUnicode = `console.log('你好,世界!');`;
  await runTest('包含 Unicode (中文) 字串的程式碼', codeUnicode);
}
main();
現在,確保你的 Next.js 開發伺服器仍在運行,然後打開另一個終端機視窗,執行我們的測試腳本:
node scripts/test-execution.js
稍等幾秒鐘,你應該會看到類似以下的輸出:
--- 測試案例: 正常執行的程式碼 ---
  ✅ 成功從代理 API 收到回應:
{
  "stdout": "Hello from the secure sandbox!\n",
  "stderr": null,
  "status": { "id": 3, "description": "Accepted" },
  "time": "0.025",
  "memory": 7964
}
--- 測試案例: 會產生執行階段錯誤 (Runtime Error) 的程式碼 ---
  ✅ 成功從代理 API 收到回應:
{
  "stdout": null,
  "stderr": "TypeError: a.toUpperCase is not a function\n    at /box/script.js:2:3\n    at ...",
  "status": { "id": 11, "description": "Runtime Error (NZEC)" },
  "time": "0.024",
  "memory": 8048
}
--- 測試案例: 語法錯誤 (Syntax Error) 的程式碼 ---
  ✅ 成功從代理 API 收到回應:
{
  "stdout": null,
  "stderr": "/box/script.js:1\nconsole.log('Missing quote);\n             ^^^^^^^^^^^^^^^\n\nSyntaxError: Invalid or unexpected token\n    at ...",
  "status": { "id": 11, "description": "Runtime Error (NZEC)" },
  "time": "0.022",
  "memory": 7764
}
--- 測試案例: 包含 Unicode (中文) 字串的程式碼 ---
  ✅ 成功從代理 API 收到回應:
{
  "stdout": "你好,世界!\n",
  "stderr": null,
  "status": { "id": 3, "description": "Accepted" },
  "time": "0.023",
  "memory": 7872
}
太棒了!這次的輸出結果完美地驗證了我們代理 API 的健壯性。所有測試案例都成功回傳了預期的結果,這代表我們的程式碼執行引擎已經準備就緒。讓我們來逐一解析每個案例背後的意義:
總結來說,我們現在有了一個極其可靠的機制,無論使用者提交的 JavaScript 程式碼是正常、有語法錯誤、還是有執行階段錯誤,我們都能取得一份詳細、客觀的執行報告。這份報告,就是我們明天要餵給 AI 的高品質「事實材料」。
今天我們完成了「理科」能力的最後一塊拼圖,讓我們的 AI 面試官專案具備了判斷程式碼客觀對錯的能力。
✅ 我們理解了 Judge0 的非同步 API 運作模式(提交 -> 輪詢 -> 取回)。
✅ 我們成功建立了 /api/judge0/execute 代理,用於安全地提交程式碼。
✅ 我們在後端實作了輪詢機制,並加上了超時保護,讓流程更穩健。
✅ 我們透過測試腳本,驗證了可以精準地取得 stdout, stderr 等客觀執行數據。
客觀事實已經到手!明天(Day 14),我們將迎來第二週的最高潮:把 RAG 的「文科知識」和 Judge0 的「理科事實」結合起來,一起餵給 Gemini。我們將強制 AI 輸出結構化的 JSON,讓它的 Code Review 既有深度,又有根據!這是讓我們的 AI 面試官真正變得「智能」的關鍵一步。
我們明天見!